﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ShortcutDal.cs" company="TechBits">
//   Copyright (c) TechBits. All rights reserved.
// </copyright>
// <author>
//   Leandro G. Vacirca.
// </author>
// <summary>
//   Defines the ShortcutDal type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace IntelliShortCut.Core
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Security;
    using System.Xml.Serialization;
    
    using Application;
    using Data;
    using log4net;

    /// <summary>
    /// Data Access Layer.
    /// </summary>
    public class ShortcutDal
    {
        #region "Private Members"

        /// <summary>
        /// Logger interface.
        /// </summary>
        private readonly ILog _logger;

        /// <summary>
        /// This will look for changes on the shortcuts xml file.
        /// </summary>
        private readonly FileWatcher _fileWatcher;

        /// <summary>
        /// Retrieve a single instance of the class.
        /// </summary>
        private static readonly ShortcutDal _dal = new ShortcutDal();

        /// <summary>
        /// Dictionary with all the shortcut data.
        /// </summary>
        private List<ShortcutData> _data;

        #endregion

        #region "Constructor"

        /// <summary>
        /// Prevents a default instance of the <see cref="ShortcutDal"/> class from being created.
        /// </summary>
        private ShortcutDal()
        {
            _logger = LogManager.GetLogger(typeof(ShortcutDal));
            _data = new List<ShortcutData>();
            
            LoadShortcutsData();

            // Checks if the DB is corrupted and if so load the backuped DB
            var shortcutsCount = GetAll().Count();
            if (ApplicationConfiguration.Settings.ShortcutsSavedCount > shortcutsCount)
            {
                LoadShortcutsData(true);
            }

            // Setup the file watcher
            _fileWatcher = new FileWatcher { FullPath = ApplicationConfiguration.XMLDataFile };
            _fileWatcher.OnFileChanged += FileWatcherOnFileChanged;
            _fileWatcher.Start();
        }

        #endregion

        #region "Events"

        /// <summary>
        /// The shortcuts DB has been changed.
        /// </summary>
        public event EventHandler<ShortcutEventArgs> OnShorcutsChanged;

        #endregion

        #region "Public Properties"

        /// <summary>
        /// Gets a single instance of the class.
        /// </summary>
        /// <returns>
        /// Single instance of the class.
        /// </returns>
        public static ShortcutDal Instance
        {
            get
            {
                return _dal;
            }
        }

        #endregion

        #region "Public Methods"

        /// <summary>
        /// Adds a new shortcut to the xml file list.
        /// </summary>
        /// <param name="shortcut">The shortcut.</param>
        /// <exception cref="ApplicationException">
        /// Throw an exception if the shortcut already exists.
        /// </exception>
        public void Add(ShortcutData shortcut)
        {
            Add(shortcut, false);
        }

        /// <summary>
        /// Adds a new shortcut to the xml file list.
        /// </summary>
        /// <param name="shortcut">The shortcut.</param>
        /// <param name="fromShell">Indicates if the shortcut must be saved to the DB.</param>
        /// <exception cref="ApplicationException">
        /// Throw an exception if the shortcut already exists.
        /// </exception>
        public void Add(ShortcutData shortcut, bool fromShell)
        {
            lock (_data)
            {
                // Checks if the shortcut is already included in the list
                if (Exists(shortcut))
                {
                    throw new ApplicationException("The shorcut already exists.");
                }

                var newShortcut = (ShortcutData) shortcut.Clone();

                // Generates the Id and the MD5 code
                newShortcut.Id = SecurityManager.GetUniqueKey();
                newShortcut.MD5Key = SecurityManager.GenerateMD5Key(newShortcut);

                // Adds the shortcut to the list
                _data.Add(newShortcut);
                _logger.DebugFormat("New shortcut added: {0}", newShortcut);

                // Makes a commit
                if (fromShell)
                {
                    Commit();
                }

                InvokeOnshortcutsChanged();
            }
        }

        /// <summary>
        /// Removes a shortcut from the xml file list.
        /// </summary>
        /// <param name="shortcut">The shortcut.</param>
        /// <exception cref="ApplicationException">
        /// Throw an exception if the shortcut does not exist in the list.
        /// </exception>
        public void Remove(ShortcutData shortcut)
        {
            lock (_data)
            {
                // Checks if the shortcut is already included in the list
                if (!Exists(shortcut))
                {
                    throw new ApplicationException("The shortcut does not exists.");
                }

                // Removes the shortcut
                var sc = _data.Single(s => s.Id == shortcut.Id);
                _data.Remove(sc);

                InvokeOnshortcutsChanged();

                _logger.DebugFormat("Shortcut removed: {0}", sc);
            }
        }

        /// <summary>
        /// Updates a shortcut to the xml file list.
        /// </summary>
        /// <param name="shortcut">The shortcut.</param>
        /// <exception cref="ApplicationException">
        /// If the shortcut does not exist in the list.
        /// </exception>
        public void Update(ShortcutData shortcut)
        {
            lock (_data)
            {
                // Checks if the shortcut is already included in the list
                if (!Exists(shortcut))
                {
                    throw new ApplicationException("The shortcut does not exists.");
                }

                // Updates the shortcut
                var sc = _data.Single(s => s.Id == shortcut.Id);
                sc.Update(shortcut);

                // Recalculates the MD5 key and update the shortcut data
                sc.MD5Key = null;
                sc.MD5Key = SecurityManager.GenerateMD5Key(sc);

                InvokeOnshortcutsChanged();

                _logger.DebugFormat("Shortcut updated: {0}", sc);
            }
        }

        /// <summary>
        /// Updates a group of shortcuts to the xml file list.
        /// </summary>
        /// <param name="shortcuts">The shortcuts.</param>
        /// <exception cref="ApplicationException">
        /// If any shortcut does not exist in the list.
        /// </exception>
        public void Update(List<ShortcutData> shortcuts)
        {
            lock (_data)
            {
                // Checks if every shortcut is already included in the list
                if (!shortcuts.All(Exists))
                {
                    throw new ApplicationException("One of the shortcuts does not exists.");
                }

                foreach (var shortcut in shortcuts)
                {
                    // Updates the shortcut
                    ShortcutData data = shortcut;

                    var sc = _data.Single(s => s.Id == data.Id);
                    sc.Update(shortcut);

                    // Recalculates the MD5 key and update the shortcut data
                    sc.MD5Key = null;
                    sc.MD5Key = SecurityManager.GenerateMD5Key(sc);
                }

                InvokeOnshortcutsChanged();
            }
        }

        /// <summary>
        /// Validates shortcuts.
        /// </summary>
        public void Validate()
        {
            ShortcutValidator.Validate(_data);
        }

        /// <summary>
        /// Gets all shortcuts.
        /// </summary>
        /// <returns>
        /// A list of shortcut data.
        /// </returns>
        public List<ShortcutData> GetAll()
        {
            return (List<ShortcutData>)_data.Clone();
        }

        /// <summary>
        /// Gets all shortcuts.
        /// </summary>
        /// <param name="active">Indicates if the shortcut is active or not.</param>
        /// <returns>
        /// A list of shortcut data of an specific state.
        /// </returns>
        public List<ShortcutData> GetAll(bool active)
        {
            return GetAll().Where(s => s.Active == active).ToList();
        }

        /// <summary>
        /// Gets all shortcuts by category.
        /// </summary>
        /// <param name="active">Indicates if the shortcut is active or not.</param>
        /// <param name="category">The categoryof the shortcut.</param>
        /// <returns>
        /// A list of shortcut data of an specific state and category.
        /// </returns>
        public List<ShortcutData> GetAll(bool active, ShortcutCategoryEnum? category)
        {
            return category == null ? GetAll(active) : GetAll().Where(s => s.Active == active && s.Category == category).ToList();
        }

        /// <summary>
        /// Gets all last or most used shortcuts.
        /// </summary>
        /// <param name="active">Indicates if the shortcut is active or not.</param>
        /// <param name="lastMostUsed">True: last used; False most used.</param>
        /// <returns>
        /// A list of shortcut data of the last or most used shortcuts.
        /// </returns>
        public List<ShortcutData> GetAll(bool active, bool lastMostUsed)
        {
            var lastUsedItemsCount = GetAll(true).Count > ApplicationConfiguration.LastUsedItemsCount
                                         ? ApplicationConfiguration.LastUsedItemsCount
                                         : GetAll(true).Count;

            var mostUsedItemsCount = GetAll(true).Count > ApplicationConfiguration.MostUsedItemsCount
                                         ? ApplicationConfiguration.MostUsedItemsCount
                                         : GetAll(true).Count;

            if (lastMostUsed)
            {
                return GetAll(active).OrderBy(s => s.LastUsed).Take(lastUsedItemsCount).ToList();
            }

            return GetAll(active).OrderByDescending(s => s.Rank).Take(mostUsedItemsCount).ToList();
        }

        /// <summary>
        /// Commits changes to the xml database.
        /// </summary>
        /// <remarks>
        /// This method must be called before closing the application.
        /// </remarks>
        public void Commit()
        {
            SaveShortcutsData();
        }

        /// <summary>
        /// Backups the shortcuts data into a backup file.
        /// </summary>
        public void BackUp()
        {
            SaveShortcutsData(true);
        }

        #endregion

        #region "Private Methods"

        /// <summary>
        /// Checks if the shortcut already exits.
        /// </summary>
        /// <param name="shortcut">The shortcut.</param>
        /// <returns>
        /// True: the shortcut exists; False: otherwise.
        /// </returns>
        /// <exception cref="ApplicationException">
        /// The shortcut is duplicated.
        /// </exception>
        private bool Exists(ShortcutData shortcut)
        {
            if (_data.SingleOrDefault(s => s.Id.Equals(shortcut.Id)) != null)
            {
                return true;
            }

            var result = _data.Where(s => s.ByNameOrAlias ? s.Name.Equals(shortcut.Name) : s.Alias.Equals(shortcut.Alias)).Count();
            if (result == 0)
            {
                return false;
            }

            if (result == 1)
            {
                return true;
            }

            throw new ApplicationException("The shorcut is duplicated.");
        }

        /// <summary>
        /// Loads the shortcuts data from the xml data file.
        /// </summary>
        private void LoadShortcutsData()
        {
            LoadShortcutsData(false);
        }

        /// <summary>
        /// Loads the shortcuts data from the xml data file.
        /// </summary>
        /// <param name="backup">Indicates if the data must be loaded from the backup file.</param>
        private void LoadShortcutsData(bool backup)
        {
            var duration = DateTime.Now;
            lock (_data)
            {
                try
                {
                    var xmlSerializer = new XmlSerializer(typeof(List<ShortcutData>));
                    var xmlFile = backup && File.Exists(ApplicationConfiguration.XMLDataBackupFile) ? ApplicationConfiguration.XMLDataBackupFile : ApplicationConfiguration.XMLDataFile;
                    using (var streamReader = new StreamReader(xmlFile))
                    {
                        _data = (List<ShortcutData>)xmlSerializer.Deserialize(streamReader);
                    }

                    // Validates shortcuts
                    ShortcutValidator.Validate(_data);
                }
                catch (Exception)
                {
                    SaveShortcutsData();
                }
                finally
                {
                    if (!SecurityManager.CheckDataIntegrity((List<ShortcutData>)_data.Clone()))
                    {
                        throw new SecurityException("The data integrity has been corrupted externally.");
                    }
                }                
            }

            _logger.DebugFormat("LoadShortcutsData duration [ms]: {0}", DateTime.Now.Subtract(duration).TotalMilliseconds);
        }

        /// <summary>
        /// Saves the shortcuts data to the xml data file.
        /// </summary>
        private void SaveShortcutsData()
        {
            SaveShortcutsData(false);
        }

        /// <summary>
        /// Saves the shortcuts data to the xml data file.
        /// </summary>
        /// <param name="backup">Indicates if the data must be saved on a backup file.</param>
        private void SaveShortcutsData(bool backup)
        {
            var duration = DateTime.Now;
            lock (_data)
            {
                var xmlSerializer = new XmlSerializer(typeof(List<ShortcutData>));
                var xmlFile = backup ? ApplicationConfiguration.XMLDataBackupFile : ApplicationConfiguration.XMLDataFile;
                using (var streamWriter = new StreamWriter(xmlFile))
                {
                    xmlSerializer.Serialize(streamWriter, _data);
                }
            }

            _logger.DebugFormat("SaveShortcutsData duration [ms]: {0}", DateTime.Now.Subtract(duration).TotalMilliseconds);
        }

        /// <summary>
        /// Event that indicates the file has changed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The event parameters.</param>
        private void FileWatcherOnFileChanged(object sender, FileWatcherEventArgs e)
        {
            LoadShortcutsData();
            InvokeOnshortcutsChanged();
        }

        /// <summary>
        /// Invokes the event that indicates the shortcuts have been changed.
        /// </summary>
        private void InvokeOnshortcutsChanged()
        {
            if (OnShorcutsChanged != null)
            {
                OnShorcutsChanged(this, new ShortcutEventArgs { Shortcuts = (List<ShortcutData>)_data.Clone() });
            }
        }

        #endregion
    }
}